#!/usr/bin/env python
"""
Axe Interpreter

An interpreter for programs written in the Axe Parser language for computers.

Author: Michael Lee (<michael.lee.0x2a@gmail.com>)
License: GNU Lesser GPL
"""

## System Modules ##
import os
import threading
import operator
import random

## 3rd Party Modules ##
import pygame

## My Modules ##
import io
import axelex
import axeparse
import meta
from meta import ExitException

_debug = False
_draw = False
_EXCLUDED_KEYS = (300, 301, 302)    # Num lock, Caps lock, Scroll lock


class Interpreter():
    def __init__(self, lexer, parser):
        #self.vars = {'A':0, 'B':0, 'C':0, 'D':0, 'E':0,
        #             'F':0, 'G':0, 'H':0, 'I':0, 'J':0,
        #             'K':0, 'L':0, 'M':0, 'N':0, 'O':0, 
        #             'P':0, 'Q':0, 'R':0, 'S':0, 'T':0, 
        #             'U':0, 'V':0, 'W':0, 'X':0, 'Y':0, 
        #             'Z':0, 'Theta':0, 'Ans':0}
        self.memory = bytearray([0 for x in range(256 * 256 - 1)])
        
        self.operators = ('+', '-', '*', '/', '<=', '>=', '<', '>', '=')
        self.emulator = io.Emulator()
        self.lexer = lexer
        self.parser = parser
        
        self._DRAW_CLRDRAW([])
        self._DRAW_DISPGRAPH([37696, 2])
        return
    
    def sanity_check(self): 
        # This is just a wrapper over Emulator.sanity_check() for convenience
        return self.emulator.sanity_check()

    def kill(self):
        # Another wrapper.
        try:
            self.emulator.kill_window()
        except ExitException:
            pass
        return
        
    
    def run(self, ast):
        ast = self.prune(ast)
        if ast == None:
            return
        ast = ast[1]
        output = self.execute(ast)
        if _draw:
            self.execute(['DRAW_DISPGRAPH', 0])
        return output
    
    def execute(self, ast):
        if _debug:
            print "execute:", ast
        
        self.emulator.sanity_check()
        
        if isinstance(ast, int):
            # Int check.
            return ast % 65536
        elif isinstance(ast, list):
            if len(ast) == 1:
                self.execute(ast[0])
            else:
                # List check.
                func = ast[0]
                args = ast[1:]
                
                value = getattr(self, '_' + func)(args)
                if _debug:
                    print "...returned:", value
                return value
        return
    
    ## Drawing Commands ##
    def _DRAW_PXL_ON(self, args):
        self.emulator.rect(self.execute(args[0]), (1, 1))
        return 0
    
    def _DRAW_PXL_OFF(self, args):
        self.emulator.rect(self.execute(args[0]), (1, 1), color=(255,255,255))
        return 0
    
    def _DRAW_PXL_TEST(self, args):
        (x, y) = self.execute(args[0])
        return self.emulator.get_pixel(x,y)
    
    def _DRAW_PXL_CHANGE(self, args):
        self.emulator.invert(self.execute(args[0]), (1,1))
        return 0
    
    def _DRAW_DISPGRAPH(self, args):
        #buffer_ptr = self.execute(args[0])
        #mode = args[1]
        #
        #temp = self.memory[buffer_ptr:buffer_ptr+768]
        #main = [(255,255,255) if x else (0,0,0) for x in temp]
        #
        #self.emulator.update_screen(main=main, mode=mode)
        self.emulator.update_screen()
        return 0
    
    def _DRAW_CLRDRAW(self, args):
        self.emulator.rect((0,0), self.emulator.size, color=(255,255,255))
        return 0
    
    def _DRAW_RECT(self, args):
        self.emulator.rect(self.execute(args[0]), self.execute(args[1]))
        return 0
    
    def _DRAW_RECTI(self, args):
        self.emulator.invert(self.execute(args[0]), self.execute(args[1]))
        return 0
    
    def _DRAW_LINE(self, args):
        self.emulator.line(self.execute(args[0]), self.execute(args[1]))
        return 0
    
    def _DRAW_HORIZONTAL(self, args):
        self.emulator.shift(args[0], args[1])
        return args[0]
    
    def _DRAW_VERTICAL(self, args):
        self.emulator.shift(args[0], args[1])
        return args[1]
    
    def _COORD(self, args):
        return (self.execute(args[0]), self.execute(args[1]))
    
    
    ## Control Structures ##
    def _WHILE(self, args):
        while self.execute(args[0]):
            self.execute(args[1])
        return 0
    
    def _REPEAT(self, args):
        while not self.execute(args[0]):
            self.execute(args[1])
        return 0
            
    def _FOR(self, args):
        loop = args[0]
        var = loop[1]
        start = loop[2]
        end = loop[3]
        
        block = args[1]
        failsafe = 0
        
        i = start
        while i != end:
            self.execute(['ASSIGN', i, var])
            self.execute(block)
            i = self.execute(var)
            i += 1
        return -1   # Not sure why (but the original does this)
    
    def _IF(self, args):
        condition = self.execute(args[0])
        if condition:
            return self.execute(args[1])
        return 0
    
    def _ELSEIF(self, args):
        for block in args:
            if self.execute(block):
                return self.execute(block[2])
        return 0
    
    def _CONDITION(self, args):
        return self.execute(args[0])
    
    def _OPERATOR(self, args):
        # TODO: implement overflow better.
        op = args[0]
        value = self.execute(args[1])
        for a in args[2:]:
            value = int(getattr(operator, op)(value, self.execute(a)))
        value %= 65536
        return value
    
    def _ASSIGN(self, args):
        # TODO: Handle constants, not just vars.
        # TODO: implement overflow.
        # Note: This handles 2-byte vars
        value = self.execute(args[0])
        pointer = self.execute(args[1][1])
        order = args[1][2]
        if order == 1:
            self.memory[pointer] = value % 256
        elif order == 2:
            self.memory[pointer] = int(value / 256)
            self.memory[pointer + 1] = value % 256
        return value
    
    def _BLOCK(self, args):
        new_args = []
        for arg in args:
            new_args.append(self.execute(arg))
        return new_args
    
    def _DISP(self, args):
        for arg in args:
            print 'OUT  :', self.execute(arg)
        return 0    # Disp doesn't return anything coherent?
    
    ### COMMANDS ###
    def _GETKEY(self, args):
        pygame.event.pump()
        keys = list(pygame.key.get_pressed())
        if args[0]:
            for key in args:
                if keys[key]:
                    return 1
        else:
            # I'm doing all this because I don't want 'GetKey(0)' to trigger
            # if the num lock or caps lock is held down.
            indices = [i for i, x in enumerate(keys) if x==1]
            if set(indices).issubset(_EXCLUDED_KEYS):
                # If every single held key is in the list of excluded keys,
                # then we're going to say that no keys are held down.
                return 0
            else:
                return 1
            return 1 if (1 in keys) else 0
        return 0
    
    def _PAUSE(self, args):
        pygame.time.delay(int((float(self.execute(args[0])*1000)/1800)))
        # In Normal mode, 1 second is about 'Pause 1800'.
        # In pygame, the delay is by milliseconds.
        return 0
    
    def _RAND(self, args):
        return random.randint(0, 256*256-1)
    
    
    ### POINTERS ###
    def _POINTER(self, args):
        pointer = self.execute(args[0])
        order = args[1]
        if order == 1:
            return self.memory[pointer]
        elif order == 2:
            return self.memory[pointer] * 256 + self.memory[pointer + 1]
    
    def _DEREFERENCE(self, args):
        pointer = self.execute(args[0][1])
        return pointer
    
    ### META ###
    def _META(self, args):
        self.extend('_META', args)
        return
    
    def _META_EXIT(self, args):
        raise ExitException(args[0])
        return
    
    def _META_DEBUG(self, args):
        global _debug
        _debug = True if args[0] else False
        return args[0]
    
    def _META_STATE(self, args):
        print self.vars
        return 0
    
    def _META_DRAW(self, args):
        global _draw
        _draw = True if args[0] else False
        return args[0]
    
    def _META_HELP(self, args):
        print meta.help
        return 0
    
    def _META_ABOUT(self, args):
        print meta.about
        return 0
    
    def _META_OPEN(self, args):
        filepath = args[0]
        possible_paths = [args[0], 
                          os.path.join(meta.curdir(), filepath),
                          os.path.join(os.getcwd(), filepath)]
        found_path = False
        for path in possible_paths:
            try:
                print path
                file_ = open(path, 'r')
                found_path = True
            except IOError:
                continue
        if not found_path:
            print 'ERROR: Unable to open file.'
            return 0
        text = file_.read()
        file_.close()
        text = text + '\n'
        # This is potentially very buggy, so I don't want to obscure
        # any exceptions.
        result = self.parser.parse(text, lexer=self.lexer)
        return self.run(result)
        
    def prune(self, ast):
        return ast
        name = ast[0]
        children = ast[1:]
        children_new = [name]
        for child in children:
            if type(child[1]) == list:
                if child[0] == 'IF' and child[1][1] == 0:
                    continue
                else:
                    new_child = self.prune(child)
                    children_new.append(new_child)
            else:
                children_new.append(child)
        return children_new

    def free(self, data):
        new_data = []
        for i in data:
            if type(i) == list:
                new_data.extend(self.free(i))
            else:
                new_data.append(i)
        return new_data
    
    def extend(self, head, args):
        return getattr(self, head + '_' + args[0])(args[1:])


def build(lexer=axelex.build(), parser=axeparse.build()):
    return Interpreter(lexer, parser)

thread_text = ''

def test_interpreter(parser=axeparse.build(),
                     lexer=axelex.build(),
                     interpreter=None):
    interpreter = Interpreter(lexer, parser) if interpreter == None else interpreter
    global thread_text
    global _draw
    _draw = True

    class _console_thread(threading.Thread):
        def run(self):
            global thread_text
            lock = threading.Lock()
            with lock:
                thread_text = raw_input('calc > ')
            return

    print "\nInterpreter ready.\n"
        
    while True:
        try:
            thread_text = ''
            t = _console_thread()
            t.start()
            while True:
                interpreter.sanity_check()
                if thread_text != '':
                    break
            t.join()
            
            text = thread_text
            
                
            # End of multiprocessing, should return to normal.
            if text[-1] not in ('\n', ':'):
                text = text + '\n'
            if text:
                ast = parser.parse(text, lexer=lexer)
                if _debug:
                    print ''
                    print ast
                print interpreter.run(ast), '\n'
        except (EOFError, KeyboardInterrupt, ExitException):
            interpreter.kill()
            print '\nEnding...\n'
            return
            
if __name__ == '__main__':
    test_interpreter()

